DRF Views Architecture

Von APIView zu ViewSets

Schritt-für-Schritt Entwicklung einer Movie API

APIView • Generic Views • ViewSets • Best Practices

📋 Was lernen wir heute?

1

Views Architecture

Überblick über DRF Views

Hierarchie verstehen

2

APIView

Die Basis aller Views

Manuelle Kontrolle

CRUD von Hand

3

Generic Views

Vorgefertigte Patterns

Weniger Code

Mixins nutzen

4

ViewSets

Alle CRUD-Operationen

Router-Integration

Custom Actions

🏗️ DRF Views Architecture - Hierarchie

4 Stufen der Abstraktion

Von maximaler Kontrolle zu maximaler Produktivität

1

🔧 APIView (Basis)

Maximale Kontrolle, maximaler Code

class MovieListAPIView(APIView):
    def get(self, request):
        # Alles manuell
        movies = Movie.objects.all()
        serializer = MovieSerializer(movies, many=True)
        return Response(serializer.data)
  • ✅ Volle Kontrolle über jeden Request
  • ✅ Gut für komplexe, individuelle Logik
  • ❌ Viel Boilerplate-Code
  • ❌ Wiederholungen für CRUD
2

🔨 Generic Views (Mixins)

Vorgefertigte Patterns, weniger Code

class MovieListView(generics.ListCreateAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    # GET + POST automatisch!
  • ✅ Weniger Code als APIView
  • ✅ Standard CRUD-Patterns
  • ✅ Kombinierbar via Mixins
  • ❌ Weniger flexibel als APIView
3

🎯 ViewSets

Alle CRUD-Operationen in einer Klasse

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    # GET (list/detail), POST, PUT, PATCH, DELETE!
  • ✅ Minimaler Code
  • ✅ Automatische URL-Generierung
  • ✅ Custom Actions möglich
  • ❌ Weniger Kontrolle als Generic Views
4

🚀 Welche wählen?

Entscheidungshilfe:

  • APIView: Komplexe, individuelle Logik (z.B. Payment, Analytics)
  • Generic Views: Standard CRUD mit Anpassungen
  • ViewSets: Standard REST-Ressourcen (90% der Fälle)

🛠️ Projekt-Setup - Ausgangssituation

Unser Ausgangspunkt

Django-Projekt mit Movie Models bereits erstellt

1. Projekt erstellt:

# Bereits ausgeführt:
django-admin startproject movieapi .
python manage.py startapp movies

# Projektstruktur:
movieapi/
├── movieapi/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── movies/
│   ├── __init__.py
│   ├── models.py      # ← Models vorhanden (Movie, Artist, MovieCasting)
│   ├── views.py       # ← Hier arbeiten wir!
│   ├── urls.py        # ← Werden wir erstellen
│   └── admin.py
└── manage.py

2. Models vorhanden:

# movies/models.py
class Movie(models.Model):
    title = models.CharField(max_length=200)
    year = models.IntegerField()
    genre = models.CharField(max_length=100, blank=True)
    rating = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True)
    description = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

class Artist(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    birth_date = models.DateField(null=True, blank=True)
    # ...

class MovieCasting(models.Model):
    movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='castings')
    artist = models.ForeignKey(Artist, on_delete=models.CASCADE, related_name='movie_roles')
    role_name = models.CharField(max_length=200)
    # ...

🎯 Was wir jetzt machen:

Wir entwickeln die Views in 3 verschiedenen Ansätzen:

  1. Mit APIView (manuell)
  2. Mit Generic Views (weniger Code)
  3. Mit ViewSets (minimaler Code)

📦 Schritt 1: DRF installieren & konfigurieren

1.1

DRF installieren

# Terminal:
pip install djangorestframework

# Output:
Successfully installed djangorestframework-3.14.0
1.2

settings.py konfigurieren

# filepath: movieapi/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    # Third-party
    'rest_framework',  # ← NEU!
    
    # Local apps
    'movies',  # ← Bereits vorhanden
]

# Am Ende der Datei hinzufügen:
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',  # Für Development
    ],
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ],
}
1.3

Migrationen ausführen

# Terminal:
python manage.py makemigrations
python manage.py migrate

# Output:
Migrations for 'movies':
  movies/migrations/0001_initial.py
    - Create model Movie
    - Create model Artist
    - Create model MovieCasting
Operations to perform:
  Apply all migrations: movies
Running migrations:
  Applying movies.0001_initial... OK

🔄 Schritt 2: Serializers erstellen

Serializers = Daten-Übersetzer

Konvertieren Models zu JSON (und zurück)

Erstelle: movies/serializers.py

# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie, Artist, MovieCasting


class MovieSerializer(serializers.ModelSerializer):
    """Serializer für Movie Model"""
    class Meta:
        model = Movie
        fields = '__all__'  # Alle Felder
        read_only_fields = ['created_at', 'updated_at']


class ArtistSerializer(serializers.ModelSerializer):
    """Serializer für Artist Model"""
    full_name = serializers.ReadOnlyField()  # Property aus Model
    
    class Meta:
        model = Artist
        fields = '__all__'
        read_only_fields = ['created_at', 'updated_at']


class MovieCastingSerializer(serializers.ModelSerializer):
    """Serializer für MovieCasting Model"""
    class Meta:
        model = MovieCasting
        fields = '__all__'
        read_only_fields = ['created_at']


class MovieCastingDetailSerializer(serializers.ModelSerializer):
    """Detaillierter Casting Serializer mit verschachtelten Objekten"""
    movie = MovieSerializer(read_only=True)
    artist = ArtistSerializer(read_only=True)
    
    class Meta:
        model = MovieCasting
        fields = '__all__'

✅ Was macht der Serializer?

  • Serialisierung: Python Movie-Objekt → JSON
  • Deserialisierung: JSON → Python Movie-Objekt
  • Validierung: Prüft Daten automatisch
  • Read-Only Felder: created_at, updated_at nicht änderbar

🔧 Ansatz 1: APIView - Die Basis

APIView = Maximale Kontrolle

Alle HTTP-Methoden manuell implementieren

✅ Vorteile

  • Volle Kontrolle über Request/Response
  • Flexibel für komplexe Logik
  • Gut verständlich (explizit)
  • Basis für alles andere

❌ Nachteile

  • Viel Boilerplate-Code
  • Wiederholungen bei CRUD
  • Mehr Code = mehr Fehler
  • Langsamer zu entwickeln

🎯 Wann APIView nutzen?

  • Individuelle Endpoints: Login, Logout, Payment
  • Komplexe Logik: Multi-Step Workflows
  • Spezielle Validierung: Business-Rules
  • Nicht-Standard REST: Custom Actions

📝 APIView: Movie List (GET)

1. View erstellen: movies/views.py

# filepath: movies/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Movie
from .serializers import MovieSerializer


class MovieListAPIView(APIView):
    """
    GET: Liste aller Filme
    """
    def get(self, request):
        """Alle Filme abrufen"""
        # 1. Daten aus DB holen
        movies = Movie.objects.all()
        
        # 2. Serialisieren (Python → JSON)
        serializer = MovieSerializer(movies, many=True)
        
        # 3. Response zurückgeben
        return Response(serializer.data, status=status.HTTP_200_OK)

2. URL registrieren: movies/urls.py (NEU erstellen)

# filepath: movies/urls.py
from django.urls import path
from .views import MovieListAPIView

urlpatterns = [
    path('movies/', MovieListAPIView.as_view(), name='movie-list'),
]

3. In Haupt-URLs einbinden: movieapi/urls.py

# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('movies.urls')),  # ← NEU!
]

🧪 Testen:

python manage.py runserver

# Browser: http://127.0.0.1:8000/api/movies/
# Output: [] (leere Liste, da noch keine Filme)

➕ APIView: Movie Create (POST)

Erweitere MovieListAPIView um POST:

# filepath: movies/views.py
class MovieListAPIView(APIView):
    """
    GET: Liste aller Filme
    POST: Film erstellen
    """
    def get(self, request):
        """Alle Filme abrufen"""
        movies = Movie.objects.all()
        serializer = MovieSerializer(movies, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)
    
    def post(self, request):
        """Film erstellen"""
        # 1. Daten aus Request holen & validieren
        serializer = MovieSerializer(data=request.data)
        
        # 2. Validierung
        if serializer.is_valid():
            # 3. Speichern
            serializer.save()
            # 4. Response zurückgeben
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        
        # Fehler zurückgeben
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

🧪 Testen mit Browsable API:

  1. Gehe zu: http://127.0.0.1:8000/api/movies/
  2. Scrolle nach unten zum Formular
  3. Fülle aus:
    {
      "title": "The Matrix",
      "year": 1999,
      "genre": "Sci-Fi",
      "rating": "8.7",
      "description": "A computer hacker..."
    }
  4. Klicke "POST"
  5. ✅ Film erstellt mit ID 1

🔍 APIView: Movie Detail (GET/PUT/PATCH/DELETE)

Neue View für einzelnen Film:

# filepath: movies/views.py
from django.shortcuts import get_object_or_404

class MovieDetailAPIView(APIView):
    """
    GET: Film-Details
    PUT: Film komplett aktualisieren
    PATCH: Film teilweise aktualisieren
    DELETE: Film löschen
    """
    
    def get_object(self, pk):
        """Helper: Film holen oder 404"""
        return get_object_or_404(Movie, pk=pk)
    
    def get(self, request, pk):
        """Film-Details abrufen"""
        movie = self.get_object(pk)
        serializer = MovieSerializer(movie)
        return Response(serializer.data, status=status.HTTP_200_OK)
    
    def put(self, request, pk):
        """Film komplett aktualisieren (alle Felder)"""
        movie = self.get_object(pk)
        serializer = MovieSerializer(movie, data=request.data)
        
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_200_OK)
        
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    def patch(self, request, pk):
        """Film teilweise aktualisieren (nur geänderte Felder)"""
        movie = self.get_object(pk)
        serializer = MovieSerializer(movie, data=request.data, partial=True)
        
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_200_OK)
        
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    def delete(self, request, pk):
        """Film löschen"""
        movie = self.get_object(pk)
        movie.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

URL hinzufügen:

# filepath: movies/urls.py
from .views import MovieListAPIView, MovieDetailAPIView

urlpatterns = [
    path('movies/', MovieListAPIView.as_view(), name='movie-list'),
    path('movies//', MovieDetailAPIView.as_view(), name='movie-detail'),  # ← NEU
]

📊 APIView - Zusammenfassung

Was haben wir mit APIView gebaut?

✅ Fertige Endpoints

GET    /api/movies/      # Liste
POST   /api/movies/      # Erstellen
GET    /api/movies/1/    # Details
PUT    /api/movies/1/    # Update (komplett)
PATCH  /api/movies/1/    # Update (teilweise)
DELETE /api/movies/1/    # Löschen

📝 Code-Statistik

  • 2 View-Klassen
  • ~70 Zeilen Code
  • 5 HTTP-Methoden manuell
  • Viel Wiederholung

✅ Positiv

  • Volle Kontrolle
  • Alles explizit & verständlich
  • Gut für Lernzwecke

❌ Negativ

  • Viel Boilerplate
  • get_object() wiederholt
  • Validierung wiederholt
  • Fehleranfällig

💡 Lösung: Generic Views!

Vorgefertigte Patterns für Standard-CRUD → Weniger Code, gleiche Funktionalität

🔨 Ansatz 2: Generic Views

Generic Views = Vorgefertigte Patterns

DRF bietet Klassen für Standard-CRUD-Operationen

🎯 Konzept

DRF stellt fertige View-Klassen bereit:

  • ListAPIView: GET Liste
  • CreateAPIView: POST
  • RetrieveAPIView: GET Detail
  • UpdateAPIView: PUT/PATCH
  • DestroyAPIView: DELETE
  • ListCreateAPIView: GET + POST
  • RetrieveUpdateDestroyAPIView: GET + PUT + PATCH + DELETE

🧩 Mixins

Die Bausteine hinter Generic Views:

  • ListModelMixin
  • CreateModelMixin
  • RetrieveModelMixin
  • UpdateModelMixin
  • DestroyModelMixin

Können selbst kombiniert werden!

✅ Vorteile

  • 90% weniger Code
  • Keine Wiederholungen
  • Best Practices eingebaut
  • Trotzdem anpassbar

❌ Nachteile

  • Weniger Kontrolle als APIView
  • Mehr zu lernen
  • Manchmal "zu viel Magie"

🛠️ Generic Views - Implementation

Ersetze APIView durch Generic Views:

# filepath: movies/views.py
from rest_framework import generics
from .models import Movie
from .serializers import MovieSerializer


class MovieListCreateView(generics.ListCreateAPIView):
    """
    GET: Liste aller Filme
    POST: Film erstellen
    """
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    # Fertig! GET + POST funktionieren automatisch!


class MovieRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
    """
    GET: Film-Details
    PUT: Film komplett aktualisieren
    PATCH: Film teilweise aktualisieren
    DELETE: Film löschen
    """
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    # Fertig! GET + PUT + PATCH + DELETE automatisch!

URLs aktualisieren:

# filepath: movies/urls.py
from django.urls import path
from .views import MovieListCreateView, MovieRetrieveUpdateDestroyView

urlpatterns = [
    path('movies/', MovieListCreateView.as_view(), name='movie-list'),
    path('movies//', MovieRetrieveUpdateDestroyView.as_view(), name='movie-detail'),
]

❌ Vorher (APIView)

~70 Zeilen Code

  • 2 View-Klassen
  • 5 Methoden manuell
  • get_object() Helper

✅ Nachher (Generic Views)

~10 Zeilen Code

  • 2 View-Klassen
  • Nur queryset & serializer
  • Alles andere automatisch!

🎨 Generic Views - Anpassungen

Generic Views sind trotzdem flexibel!

Überschreibe Methoden für Custom-Logik

Beispiel 1: Filtering

class MovieListCreateView(generics.ListCreateAPIView):
    serializer_class = MovieSerializer
    
    def get_queryset(self):
        """Custom QuerySet: Nur Filme ab 2000"""
        queryset = Movie.objects.all()
        
        # Query-Parameter auslesen
        year = self.request.query_params.get('year')
        if year:
            queryset = queryset.filter(year__gte=year)
        
        return queryset

# Verwendung: GET /api/movies/?year=2010

Beispiel 2: Custom Create (z.B. User zuweisen)

class MovieListCreateView(generics.ListCreateAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    def perform_create(self, serializer):
        """Überschreibe Speicher-Logik"""
        # Beispiel: User aus Request hinzufügen
        # serializer.save(created_by=self.request.user)
        
        # Oder Custom-Validierung
        if serializer.validated_data['year'] < 1888:
            raise ValidationError("Jahr zu früh!")
        
        serializer.save()

Beispiel 3: Custom Response

from rest_framework.response import Response

class MovieRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    def destroy(self, request, *args, **kwargs):
        """Custom DELETE mit zusätzlicher Info"""
        instance = self.get_object()
        title = instance.title
        
        self.perform_destroy(instance)
        
        return Response({
            'message': f'Film "{title}" wurde gelöscht',
            'success': True
        }, status=status.HTTP_200_OK)

📚 Generic Views - Alle Varianten

1. ListAPIView

Nur GET Liste

class MovieListView(generics.ListAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# GET /api/movies/ ✅

2. CreateAPIView

Nur POST

class MovieCreateView(generics.CreateAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# POST /api/movies/ ✅

3. RetrieveAPIView

Nur GET Detail

class MovieDetailView(generics.RetrieveAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# GET /api/movies/1/ ✅

4. UpdateAPIView

Nur PUT/PATCH

class MovieUpdateView(generics.UpdateAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# PUT /api/movies/1/ ✅
# PATCH /api/movies/1/ ✅

5. DestroyAPIView

Nur DELETE

class MovieDeleteView(generics.DestroyAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# DELETE /api/movies/1/ ✅

6. ListCreateAPIView

GET + POST

class MovieListCreateView(generics.ListCreateAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# GET /api/movies/ ✅
# POST /api/movies/ ✅

7. RetrieveUpdateAPIView

GET + PUT + PATCH

class MovieRUView(generics.RetrieveUpdateAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# GET /api/movies/1/ ✅
# PUT /api/movies/1/ ✅
# PATCH /api/movies/1/ ✅

8. RetrieveUpdateDestroyAPIView

GET + PUT + PATCH + DELETE

class MovieRUDView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# GET /api/movies/1/ ✅
# PUT /api/movies/1/ ✅
# PATCH /api/movies/1/ ✅
# DELETE /api/movies/1/ ✅

🧩 Mixins - Die Bausteine

Mixins = Wiederverwendbare Funktionalität

Generic Views sind Kombinationen aus Mixins + GenericAPIView

Die 5 Standard-Mixins:

from rest_framework import mixins

# 1. ListModelMixin - GET Liste
# 2. CreateModelMixin - POST
# 3. RetrieveModelMixin - GET Detail
# 4. UpdateModelMixin - PUT/PATCH
# 5. DestroyModelMixin - DELETE

Beispiel: Eigene Kombination bauen

from rest_framework import mixins, generics

class MovieCustomView(mixins.ListModelMixin,
                      mixins.CreateModelMixin,
                      mixins.DestroyModelMixin,
                      generics.GenericAPIView):
    """
    Custom: Nur GET Liste, POST und DELETE
    (kein PUT/PATCH)
    """
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    def get(self, request, *args, **kwargs):
        if 'pk' in kwargs:
            # DELETE braucht get_object, aber wir haben kein RetrieveMixin
            return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
        return self.list(request, *args, **kwargs)
    
    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)
    
    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)

💡 Merke:

Generic Views = Mixins + GenericAPIView

Du kannst Mixins selbst kombinieren für Custom-Kombinationen!

🎯 Ansatz 3: ViewSets - Die Lösung

ViewSets = Alle CRUD in einer Klasse

Minimaler Code, maximale Funktionalität

✅ Vorteile

  • Alle CRUD-Operationen in 1 Klasse
  • Automatische URL-Generierung (Router)
  • Custom Actions möglich
  • Minimaler Code (~5 Zeilen)
  • Standard für REST-APIs

❌ Nachteile

  • Weniger Kontrolle
  • Mehr Abstraktion
  • Schwerer zu debuggen
  • Nicht für alle Use-Cases

🔥 ViewSet-Arten

  • ViewSet: Basis (leer, alles manuell)
  • GenericViewSet: Basis + Mixins kombinierbar
  • ModelViewSet: Volle CRUD (list, create, retrieve, update, destroy)
  • ReadOnlyModelViewSet: Nur list + retrieve

🎯 Wann ViewSets nutzen?

  • Standard REST-Ressourcen (90% der Fälle)
  • CRUD-Operationen
  • Wenn Router genutzt werden soll
  • Schnelle Entwicklung

🚀 ViewSets - ModelViewSet Implementation

1. ViewSet erstellen:

# filepath: movies/views.py
from rest_framework import viewsets
from .models import Movie, Artist, MovieCasting
from .serializers import MovieSerializer, ArtistSerializer, MovieCastingSerializer


class MovieViewSet(viewsets.ModelViewSet):
    """
    ViewSet für Movie Model
    
    Automatisch verfügbar:
    - list (GET /api/movies/)
    - create (POST /api/movies/)
    - retrieve (GET /api/movies/{id}/)
    - update (PUT /api/movies/{id}/)
    - partial_update (PATCH /api/movies/{id}/)
    - destroy (DELETE /api/movies/{id}/)
    """
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    # Fertig! Alle CRUD-Operationen funktionieren!


class ArtistViewSet(viewsets.ModelViewSet):
    """ViewSet für Artist Model"""
    queryset = Artist.objects.all()
    serializer_class = ArtistSerializer


class MovieCastingViewSet(viewsets.ModelViewSet):
    """ViewSet für MovieCasting Model"""
    queryset = MovieCasting.objects.all()
    serializer_class = MovieCastingSerializer

2. Router konfigurieren:

# filepath: movies/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet, ArtistViewSet, MovieCastingViewSet

# Router erstellen
router = DefaultRouter()

# ViewSets registrieren
router.register(r'movies', MovieViewSet, basename='movie')
router.register(r'artists', ArtistViewSet, basename='artist')
router.register(r'castings', MovieCastingViewSet, basename='casting')

# URLs
urlpatterns = [
    path('', include(router.urls)),
]

🎉 Automatisch generierte URLs:

GET    /api/movies/           # Liste
POST   /api/movies/           # Erstellen
GET    /api/movies/1/         # Details
PUT    /api/movies/1/         # Update (komplett)
PATCH  /api/movies/1/         # Update (teilweise)
DELETE /api/movies/1/         # Löschen

# Gleiche für artists/ und castings/

🎨 ViewSets - Custom Actions

Eigene Endpoints hinzufügen

ViewSets können mit @action erweitert werden

Beispiel: Custom Actions für Movies

# filepath: movies/views.py
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    @action(detail=False, methods=['get'])
    def top_rated(self, request):
        """
        Custom Endpoint: GET /api/movies/top_rated/
        Top 10 bestbewertete Filme
        """
        movies = Movie.objects.filter(
            rating__isnull=False
        ).order_by('-rating')[:10]
        
        serializer = self.get_serializer(movies, many=True)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def recent(self, request):
        """
        Custom Endpoint: GET /api/movies/recent/
        Filme der letzten 5 Jahre
        """
        from datetime import datetime
        current_year = datetime.now().year
        movies = Movie.objects.filter(year__gte=current_year - 5)
        
        serializer = self.get_serializer(movies, many=True)
        return Response(serializer.data)
    
    @action(detail=True, methods=['get'])
    def castings(self, request, pk=None):
        """
        Custom Endpoint: GET /api/movies/{id}/castings/
        Besetzung eines Films
        """
        movie = self.get_object()
        castings = movie.castings.all()
        serializer = MovieCastingSerializer(castings, many=True)
        return Response(serializer.data)
    
    @action(detail=True, methods=['post'])
    def add_to_favorites(self, request, pk=None):
        """
        Custom Endpoint: POST /api/movies/{id}/add_to_favorites/
        Film zu Favoriten hinzufügen
        """
        movie = self.get_object()
        # Hier könnte Favoriten-Logik sein
        # request.user.favorites.add(movie)
        
        return Response({
            'status': 'Film zu Favoriten hinzugefügt',
            'movie': movie.title
        }, status=status.HTTP_200_OK)

🔧 @action Parameter erklärt

detail=False (Collection-Level)

URL ohne ID

@action(detail=False, methods=['get'])
def top_rated(self, request):
    # ...
    return Response(data)

# URL: /api/movies/top_rated/
# Arbeitet mit ganzer Collection

Verwendung:

  • Listen-Operationen
  • Bulk-Actions
  • Statistiken

detail=True (Instance-Level)

URL mit ID

@action(detail=True, methods=['get'])
def castings(self, request, pk=None):
    movie = self.get_object()
    # ...
    return Response(data)

# URL: /api/movies/{id}/castings/
# Arbeitet mit einem Objekt

Verwendung:

  • Objekt-spezifische Actions
  • Beziehungen
  • Status-Änderungen

methods=['get']

HTTP-Methode(n)

@action(detail=True, methods=['get'])
def stats(self, request, pk=None):
    # GET /api/movies/{id}/stats/
    pass

@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
    # POST /api/movies/{id}/publish/
    pass

@action(detail=False, methods=['get', 'post'])
def bulk_import(self, request):
    # GET & POST /api/movies/bulk_import/
    pass

url_path & url_name

Custom URL-Namen

@action(
    detail=True,
    methods=['post'],
    url_path='mark-as-favorite',  # URL
    url_name='mark_favorite'      # Reverse-URL
)
def mark_favorite(self, request, pk=None):
    # POST /api/movies/{id}/mark-as-favorite/
    pass

# Reverse:
# reverse('movie-mark_favorite', args=[1])

👁️ ReadOnlyModelViewSet

Nur Lesen, kein Schreiben

Für Read-Only APIs (z.B. öffentliche Daten)

Beispiel: Öffentliche Movie-API (nur Lesen)

# filepath: movies/views.py
from rest_framework import viewsets

class PublicMovieViewSet(viewsets.ReadOnlyModelViewSet):
    """
    Öffentliche Movie-API (nur Lesen)
    
    Verfügbar:
    - list (GET /api/public/movies/)
    - retrieve (GET /api/public/movies/{id}/)
    
    NICHT verfügbar:
    - create (POST) ❌
    - update (PUT/PATCH) ❌
    - destroy (DELETE) ❌
    """
    queryset = Movie.objects.filter(rating__gte=7.0)  # Nur gute Filme
    serializer_class = MovieSerializer

URLs:

# filepath: movies/urls.py
from .views import PublicMovieViewSet

public_router = DefaultRouter()
public_router.register(r'public/movies', PublicMovieViewSet, basename='public-movie')

urlpatterns = [
    path('', include(router.urls)),          # Admin-API
    path('', include(public_router.urls)),   # Public-API
]

🎯 Use Cases:

  • Öffentliche APIs (ohne Authentication)
  • Archiv-Daten (historisch, nicht änderbar)
  • Reporting-Endpoints

⚖️ ViewSets vs Generic Views - Vergleich

Generic Views

# 2 separate Klassen
class MovieListCreateView(generics.ListCreateAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

class MovieDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# URLs manuell
urlpatterns = [
    path('movies/', MovieListCreateView.as_view()),
    path('movies//', MovieDetailView.as_view()),
]

✅ Vorteile:

  • Mehr Kontrolle
  • Explizite URLs
  • Einfacher zu verstehen

❌ Nachteile:

  • Mehr Code
  • URLs manuell
  • Wiederholungen

ViewSets

# 1 Klasse für alles
class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# Router generiert URLs automatisch
router = DefaultRouter()
router.register(r'movies', MovieViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

✅ Vorteile:

  • Minimaler Code
  • URLs automatisch
  • Custom Actions
  • Standard für REST

❌ Nachteile:

  • Weniger Kontrolle
  • Mehr Abstraktion

🎯 Empfehlung:

  • ViewSets: Standard für REST-APIs (90% der Fälle)
  • Generic Views: Wenn mehr Kontrolle nötig (Custom-Endpoints)
  • APIView: Für komplett individuelle Logik

🔍 Filtering & Ordering in ViewSets

ViewSet mit Filtering erweitern:

# filepath: movies/views.py
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    # Filter Backends aktivieren
    filter_backends = [
        DjangoFilterBackend,   # Exakte Filter
        filters.SearchFilter,  # Textsuche
        filters.OrderingFilter # Sortierung
    ]
    
    # Welche Felder können gefiltert werden?
    filterset_fields = ['year', 'genre']
    
    # Welche Felder können durchsucht werden?
    search_fields = ['title', 'description']
    
    # Welche Felder können sortiert werden?
    ordering_fields = ['year', 'rating', 'title']
    
    # Standard-Sortierung
    ordering = ['-year']  # Neueste zuerst

Verwendung:

# Filtering (exakt):
GET /api/movies/?year=2010
GET /api/movies/?genre=Sci-Fi
GET /api/movies/?year=2010&genre=Action

# Search (enthält):
GET /api/movies/?search=Matrix
GET /api/movies/?search=Inception

# Ordering:
GET /api/movies/?ordering=year        # Aufsteigend
GET /api/movies/?ordering=-year       # Absteigend
GET /api/movies/?ordering=-rating,title  # Mehrfach

# Kombiniert:
GET /api/movies/?year=2010&search=sci&ordering=-rating

⚙️ django-filter installieren:

pip install django-filter

# settings.py:
INSTALLED_APPS = [
    # ...
    'django_filters',
]

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
    ]
}

📄 Pagination in ViewSets

Große Listen automatisch aufteilen

Verhindert zu große Responses

1. Global in settings.py konfigurieren:

# filepath: movieapi/settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

2. Pro ViewSet überschreiben:

# filepath: movies/views.py
from rest_framework.pagination import PageNumberPagination

class MoviePagination(PageNumberPagination):
    page_size = 20
    page_size_query_param = 'page_size'
    max_page_size = 100

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    pagination_class = MoviePagination  # Custom Pagination

3. Response Format:

GET /api/movies/?page=2

# Response:
{
  "count": 145,
  "next": "http://api.example.org/movies/?page=3",
  "previous": "http://api.example.org/movies/?page=1",
  "results": [
    {"id": 11, "title": "Movie 11", ...},
    {"id": 12, "title": "Movie 12", ...},
    // ... 20 Einträge
  ]
}

🎨 Andere Pagination-Typen:

  • PageNumberPagination: ?page=2
  • LimitOffsetPagination: ?limit=20&offset=40
  • CursorPagination: ?cursor=cD0yMDIz... (Performance!)

🎨 get_queryset() - Custom Filtering

Dynamisches QuerySet basierend auf User/Request

Beispiel 1: User-spezifische Filme

class MovieViewSet(viewsets.ModelViewSet):
    serializer_class = MovieSerializer
    
    def get_queryset(self):
        """Nur Filme des aktuellen Users"""
        user = self.request.user
        
        if user.is_staff:
            # Admins sehen alles
            return Movie.objects.all()
        else:
            # Normale User nur eigene
            return Movie.objects.filter(created_by=user)

Beispiel 2: Query-Parameter verarbeiten

class MovieViewSet(viewsets.ModelViewSet):
    serializer_class = MovieSerializer
    
    def get_queryset(self):
        """Custom Filter via Query-Params"""
        queryset = Movie.objects.all()
        
        # ?year_min=2010
        year_min = self.request.query_params.get('year_min')
        if year_min:
            queryset = queryset.filter(year__gte=year_min)
        
        # ?year_max=2020
        year_max = self.request.query_params.get('year_max')
        if year_max:
            queryset = queryset.filter(year__lte=year_max)
        
        # ?has_rating=true
        has_rating = self.request.query_params.get('has_rating')
        if has_rating == 'true':
            queryset = queryset.filter(rating__isnull=False)
        
        return queryset

# Verwendung:
# GET /api/movies/?year_min=2010&year_max=2020&has_rating=true

Beispiel 3: Performance-Optimierung

class MovieViewSet(viewsets.ModelViewSet):
    serializer_class = MovieSerializer
    
    def get_queryset(self):
        """Mit select_related für Performance"""
        action = self.action
        
        if action == 'list':
            # Liste: Weniger Daten
            return Movie.objects.all().only('id', 'title', 'year', 'rating')
        
        elif action == 'retrieve':
            # Details: Alle Daten + Related
            return Movie.objects.all().select_related().prefetch_related('castings')
        
        return Movie.objects.all()

🔄 get_serializer_class() - Dynamische Serializer

Verschiedene Serializer für verschiedene Actions

Beispiel: Liste vs. Detail

# filepath: movies/serializers.py
class MovieListSerializer(serializers.ModelSerializer):
    """Einfacher Serializer für Listen"""
    class Meta:
        model = Movie
        fields = ['id', 'title', 'year', 'genre', 'rating']


class MovieDetailSerializer(serializers.ModelSerializer):
    """Detaillierter Serializer für Einzelansicht"""
    castings = MovieCastingDetailSerializer(many=True, read_only=True)
    
    class Meta:
        model = Movie
        fields = '__all__'
# filepath: movies/views.py
class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    
    def get_serializer_class(self):
        """Wähle Serializer basierend auf Action"""
        if self.action == 'list':
            return MovieListSerializer  # Einfach für Listen
        return MovieDetailSerializer    # Detailliert für Details/Create/Update

Beispiel 2: Create vs. Read

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    
    def get_serializer_class(self):
        """Verschiedene Serializer für Create/Read"""
        if self.action in ['create', 'update', 'partial_update']:
            return MovieCreateSerializer  # Mit Validierung
        return MovieSerializer            # Mit verschachtelten Daten

🎯 Warum verschiedene Serializer?

  • Performance: Listen brauchen weniger Daten
  • Sicherheit: Nicht alle Felder beim Create/Update
  • Usability: Verschachtelte Daten nur bei Details

🛠️ perform_*() - Custom Save/Delete Logik

Hook-Methoden für zusätzliche Logik

1. perform_create() - Bei Erstellung

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    def perform_create(self, serializer):
        """Custom Logik beim Erstellen"""
        # User automatisch hinzufügen
        serializer.save(created_by=self.request.user)
        
        # Oder: Logging
        movie = serializer.save()
        logger.info(f"Film erstellt: {movie.title} von {self.request.user}")
        
        # Oder: Zusätzliche Validierung
        if serializer.validated_data['year'] < 1888:
            raise ValidationError("Film zu alt!")

2. perform_update() - Bei Update

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    def perform_update(self, serializer):
        """Custom Logik beim Update"""
        # Alte Werte merken
        old_title = serializer.instance.title
        
        # Speichern
        movie = serializer.save()
        
        # Logging
        if old_title != movie.title:
            logger.info(f"Film umbenannt: {old_title} → {movie.title}")
        
        # Notification senden
        # send_notification(f"Film {movie.title} wurde aktualisiert")

3. perform_destroy() - Bei Löschung

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    def perform_destroy(self, instance):
        """Custom Logik beim Löschen"""
        # Prüfung
        if instance.castings.count() > 0:
            raise ValidationError("Film hat noch Besetzungen!")
        
        # Logging
        logger.warning(f"Film gelöscht: {instance.title}")
        
        # Archivieren statt löschen
        instance.is_deleted = True
        instance.save()
        
        # Oder wirklich löschen:
        # instance.delete()

🔒 Permissions in ViewSets

1. Global Permissions

from rest_framework.permissions import IsAuthenticated, IsAdminUser

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    permission_classes = [IsAuthenticated]  # Nur eingeloggte User

2. Permissions pro Action

from rest_framework.permissions import IsAuthenticatedOrReadOnly

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    def get_permissions(self):
        """Verschiedene Permissions pro Action"""
        if self.action in ['list', 'retrieve']:
            # Lesen für alle
            permission_classes = [AllowAny]
        elif self.action in ['create', 'update', 'partial_update']:
            # Schreiben nur für eingeloggte
            permission_classes = [IsAuthenticated]
        else:
            # Löschen nur für Admins
            permission_classes = [IsAdminUser]
        
        return [permission() for permission in permission_classes]

3. Custom Permission

# filepath: movies/permissions.py
from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    """Nur Owner kann ändern"""
    
    def has_object_permission(self, request, view, obj):
        # Lesen für alle
        if request.method in permissions.SAFE_METHODS:
            return True
        
        # Schreiben nur für Owner
        return obj.created_by == request.user


# In ViewSet:
class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    permission_classes = [IsOwnerOrReadOnly]

📊 Code-Vergleich: Alle 3 Ansätze

❌ 1. APIView

~70 Zeilen Code

class MovieListAPIView(APIView):
    def get(self, request):
        movies = Movie.objects.all()
        serializer = MovieSerializer(movies, many=True)
        return Response(serializer.data)
    
    def post(self, request):
        serializer = MovieSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)

class MovieDetailAPIView(APIView):
    def get_object(self, pk):
        return get_object_or_404(Movie, pk=pk)
    
    def get(self, request, pk):
        movie = self.get_object(pk)
        serializer = MovieSerializer(movie)
        return Response(serializer.data)
    
    def put(self, request, pk):
        movie = self.get_object(pk)
        serializer = MovieSerializer(movie, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=400)
    
    def delete(self, request, pk):
        movie = self.get_object(pk)
        movie.delete()
        return Response(status=204)

# URLs:
urlpatterns = [
    path('movies/', MovieListAPIView.as_view()),
    path('movies//', MovieDetailAPIView.as_view()),
]

📝 2. Generic Views

~10 Zeilen Code

from rest_framework import generics

class MovieListCreateView(generics.ListCreateAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

class MovieDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# URLs:
urlpatterns = [
    path('movies/', MovieListCreateView.as_view()),
    path('movies//', MovieDetailView.as_view()),
]

✅ 3. ViewSets

~5 Zeilen Code

from rest_framework import viewsets

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# URLs (Router):
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'movies', MovieViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

🎯 Alle 3 bieten die gleichen Endpoints:

GET    /api/movies/      ✅
POST   /api/movies/      ✅
GET    /api/movies/1/    ✅
PUT    /api/movies/1/    ✅
PATCH  /api/movies/1/    ✅
DELETE /api/movies/1/    ✅

💡 Best Practices - Views Architecture

1. Wann was nutzen?

  • ViewSets: 90% der Fälle (Standard CRUD)
  • Generic Views: Wenn mehr Kontrolle nötig
  • APIView: Komplett individuelle Logik

2. Performance

  • select_related() für ForeignKeys
  • prefetch_related() für Many-to-Many
  • .only() für Listen (nur benötigte Felder)
  • ✅ Pagination immer aktivieren
  • ✅ Database Indexes auf Filter-Felder

3. Serializer

  • ✅ Verschiedene Serializer für List/Detail
  • read_only_fields für Timestamps
  • ✅ Validierung in Serializer, nicht in View
  • ❌ Nicht zu viele verschachtelte Serializer

4. Custom Actions

  • @action für nicht-CRUD-Operationen
  • detail=True für einzelne Objekte
  • detail=False für Collections
  • ✅ Sprechende Namen: top_rated, recent

5. Filtering

  • ✅ django-filter für komplexe Queries
  • ✅ SearchFilter für Textsuche
  • ✅ OrderingFilter für Sortierung
  • ✅ Nicht zu viele filterset_fields

6. Sicherheit

  • ✅ Permissions immer setzen
  • ✅ Authentication in Produktion
  • ✅ Throttling für Rate-Limiting
  • ❌ Nicht AllowAny in Produktion

🎯 Zusammenfassung

Was haben wir gelernt?

📜 Views Architecture

  • ✅ 3 Abstraktions-Stufen verstanden
  • ✅ APIView (maximale Kontrolle)
  • ✅ Generic Views (weniger Code)
  • ✅ ViewSets (minimaler Code)

🛠️ Implementierung

  • ✅ Movie-API mit allen 3 Ansätzen gebaut
  • ✅ Router für automatische URLs
  • ✅ Custom Actions erstellt
  • ✅ Filtering & Pagination

🎨 Anpassungen

  • get_queryset() überschrieben
  • get_serializer_class() dynamisch
  • perform_create/update/destroy()
  • ✅ Permissions konfiguriert

💡 Best Practices

  • ✅ ViewSets als Standard
  • ✅ Performance-Optimierung
  • ✅ Sicherheit beachten
  • ✅ Filtering & Pagination nutzen

❌ APIView

~70 Zeilen Code

Viel Boilerplate

📝 Generic Views

~10 Zeilen Code

Gute Balance

✅ ViewSets

~5 Zeilen Code

Production-Ready!

🚀 Nächste Schritte

1

Authentication

JWT Tokens implementieren

Login/Logout Endpoints

2

Permissions

Granulare Zugriffsrechte

Custom Permissions

3

Testing

API-Tests schreiben

APITestCase nutzen

4

Dokumentation

Swagger/OpenAPI

drf-spectacular

5

Deployment

Production-Ready

Docker, HTTPS, CORS

🎉 Gratulation!

Du kennst jetzt alle DRF Views-Ansätze!

✅ Was du jetzt kannst:

  • 🔧 APIView für individuelle Logik
  • 🔨 Generic Views für Standard-Patterns
  • 🎯 ViewSets für produktive APIs
  • 🎨 Custom Actions erstellen
  • 🔍 Filtering & Ordering
  • 📄 Pagination implementieren
  • 🔒 Permissions setzen
  • 💡 Best Practices anwenden

🎯 Empfehlung:

Nutze ViewSets als Standard!

  • Minimaler Code
  • Automatische URLs
  • Custom Actions
  • Production-Ready

Nur bei sehr speziellen Anforderungen auf Generic Views oder APIView zurückgreifen.

📚 Weiterführende Ressourcen:

  • DRF Dokumentation: https://www.django-rest-framework.org/
  • ViewSets Guide: https://www.django-rest-framework.org/api-guide/viewsets/
  • Generic Views: https://www.django-rest-framework.org/api-guide/generic-views/

Viel Erfolg mit deinen APIs! 🚀

1 / 33